package org.spindle.soasuite.controllers;


import org.spindle.soasuite.core.api.AExecutable;
import org.spindle.soasuite.core.api.annotations.Executable;
import org.spindle.soasuite.core.api.annotations.Parameter;
import org.spindle.soasuite.core.api.annotations.ParameterList;
import org.spindle.soasuite.core.constants.StateConstants;
import org.spindle.soasuite.core.logging.Log;
import org.spindle.soasuite.core.logging.LogFactory;
import org.spindle.soasuite.data.logic.FieldParameter;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.*;

public class ControllerFactory {

    private static Log log = LogFactory.getLogger(ControllerFactory.class);

    private final static String currentPackage = ControllerFactory.class.getPackage().getName();

    private Map<String, Class> controllerClass;

    private Map<String, AExecutable> executablesMap;

    private Map<String, List<FieldParameter>> requestParameters = new HashMap<String, List<FieldParameter>>();


    public void initialize() throws IOException, ClassNotFoundException {

        String path = null;
        Boolean isExecutable = false;
        Class[] classes = getClasses(currentPackage);
        controllerClass = new HashMap<String, Class>(classes.length);

        for (Class klass : classes) {
            isExecutable = false;
            for (Annotation annotation : klass.getAnnotations()) {
                if (annotation instanceof Executable) {
                    path = ((Executable) annotation).path();
                    controllerClass.put(path, klass);
                    log.logDebug("Registered path " + path + " for " + klass.getSimpleName());
                    isExecutable = true;
                }
            }

            if (isExecutable) {
                List<FieldParameter> parameterList = null;
                FieldParameter param;
                for (Field field : klass.getDeclaredFields()) {
                    for (Annotation fieldAnnot : field.getDeclaredAnnotations()) {

                        if (parameterList == null) {
                            parameterList = new ArrayList<FieldParameter>();
                        }

                        if (fieldAnnot instanceof Parameter) {
                            parameterList.add(new FieldParameter(field, ((Parameter) fieldAnnot).name()));
                        } else if (fieldAnnot instanceof ParameterList) {
                            param = new FieldParameter(field, "list");
                            param.setList(true);
                            parameterList.add(param);
                        }
                    }
                }
                requestParameters.put(path, parameterList);
            }
        }
    }

    public void populateFields(String path, Object instance, Map<String, String[]> parameterMap, Boolean endpoint) throws Exception {


        List<FieldParameter> parameters = null;
        if (endpoint) {
            parameters = requestParameters.get(StateConstants.ENDPOINT);
        } else {
            parameters = requestParameters.get(path);
        }

        String[] values;

        if (parameters != null) {
            for (FieldParameter field : parameters) {
                if (!field.getList()) {
                    values = parameterMap.get(field.getParameterName());
                    if (values != null) {
                        if (values.length > 1) {
                            throw new RuntimeException("Should not be an n-definite array");
                        }
                        field.getField().set(instance, values[0]);
                    }
                } else {
                    field.getField().set(instance, parameterMap);
                }
            }
        }
    }

    public AExecutable getController(String path, Map<String, Object> stateMapObject) {

        AExecutable executable = null;
        Class executableClass;
        String actualPath = null;

        if (path.indexOf("/") == 0) {
            path = path.substring(1);
        }

        int index;
        if ((index = path.indexOf("/")) == -1) {
            actualPath = path;
        } else {
            actualPath = path.substring(0, index);
            path = path.substring(index + 1, path.length());
        }

        if (executablesMap == null) {
            executablesMap = new HashMap<String, AExecutable>(controllerClass.size());
        }

        executableClass = controllerClass.get("/" + actualPath);
        if (executableClass != null) {
            try {
                executable = (AExecutable) executableClass.newInstance();
                executable.setPath(path);
                executable.setStateMap(stateMapObject);
                executablesMap.put(path, executable);
            } catch (InstantiationException e) {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            } catch (IllegalAccessException e) {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }
        }

        return executable;
    }

    /**
     * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
     *
     * @param packageName The base package
     * @return The classes
     * @throws ClassNotFoundException
     * @throws IOException
     */
    private Class[] getClasses(String packageName)
            throws ClassNotFoundException, IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        assert classLoader != null;
        String path = packageName.replace('.', '/');
        Enumeration<URL> resources = classLoader.getResources(path);
        List<File> dirs = new ArrayList<File>();
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement();
            dirs.add(new File(resource.getFile()));
        }
        ArrayList<Class> classes = new ArrayList<Class>();
        for (File directory : dirs) {
            classes.addAll(findClasses(directory, packageName));
        }
        return classes.toArray(new Class[classes.size()]);
    }

    /**
     * Recursive method used to find all classes in a given directory and subdirs.
     *
     * @param directory   The base directory
     * @param packageName The package name for classes found inside the base directory
     * @return The classes
     * @throws ClassNotFoundException
     */
    private List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException {
        List<Class> classes = new ArrayList<Class>();
        if (!directory.exists()) {
            return classes;
        }
        File[] files = directory.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                assert !file.getName().contains(".");
                classes.addAll(findClasses(file, packageName + "." + file.getName()));
            } else if (file.getName().endsWith(".class")) {
                classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
            }
        }
        return classes;
    }


}
